package cloudspace.vm;

import java.io.File;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javassist.ClassPath;
import javassist.ClassPool;
import javassist.Loader;
import javassist.NotFoundException;

import org.zkoss.zk.ui.Executions;

import bsh.Interpreter;
import cloudspace.vm.io.console.InfoStream;
import cloudspace.vm.io.console.SystemStreamMultiplexer;
import cloudspace.vm.javassist.JavassistBootloader;
import cloudspace.vm.javassist.MalformedConfigurationException;

//TODO: implement a strategy for handling memory management
/**
 * The Class VM. This is a basic Cloudspace Virtual Machine. This virtual
 * Machine provides shared information about the specific virtual machine to
 * every virtual machine context created in this context. This virtual machine
 * holds information about. local classes, local loader, delegated classes,
 * standard output streams, and parent loaders. There is one of these per
 * instance of a virtual machine.
 */
public class VM
{
	// -------------------------------------------------------------------------
	/**
	 * The Class LoggedClassPool. This class represents a Logged class pool that
	 * can detect updates to the class files stored internally.
	 */
	private class LoggedClassPool extends ClassPool
	{
		/**
		 * The Class LoggedFile. This is a logged file implementation that logs
		 * the time stamp of the file for future checks. Functionality of this
		 * file is limited.
		 */
		private class LoggedFile extends File
		{

			/** The Constant serialVersionUID. */
			private static final long serialVersionUID = -5428711403066713117L;

			/** The time stamp of the file. */
			private long timeStamp;

			/**
			 * Instantiates a new logged file.
			 * 
			 * @param path
			 *            the location of the file.
			 */
			public LoggedFile(String path)
			{
				super(path);
				logTimeStamp();
			}

			/**
			 * instructs the file to update the logged time stamp to the current
			 * timestamp on the file
			 */
			public void logTimeStamp()
			{
				timeStamp = this.lastModified();
			}

			/**
			 * Checks if the file has been updated
			 * 
			 * @return true, if is updated
			 */
			public boolean isUpdated()
			{
				return timeStamp != this.lastModified() || !this.exists();
			}
		}

		/** The logged class path entries. */
		List<LoggedFile> loggedClassPathEntries;

		/**
		 * Instantiates a new logged class pool.
		 */
		public LoggedClassPool()
		{
			super();
			init();
		}

		public LoggedClassPool(ClassPool parent)
		{
			super(parent);
			init();
		}

		/**
		 * Initializes the class pool's variables.
		 */
		private void init()
		{
			loggedClassPathEntries = new ArrayList<LoggedFile>();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javassist.ClassPool#appendClassPath(java.lang.String)
		 */
		@Override
		public ClassPath appendClassPath(String classPath)
				throws NotFoundException
		{
			ClassPath newClassPath = super.appendClassPath(classPath);
			if (classPath.startsWith(localRoot))
			{
				File classPathDir = new File(classPath);
				if (classPath.endsWith("/*"))
				{
					classPathDir = new File(classPath.substring(0, classPath
							.length() - 2));
				}

				recurseClassPathEntry(classPathDir);
			}

			return newClassPath;
		}

		/**
		 * This private method recurses through the classpath entries to log
		 * time stamps for every file in the class path.
		 * 
		 * @param classPathDir
		 *            the class path directory to recurse and log.
		 */
		private void recurseClassPathEntry(File classPathDir)
		{
			String[] dirList = classPathDir.list();
			if (dirList != null)
			{
				for (String classPathEntry : classPathDir.list())
				{
					LoggedFile tempFile = new LoggedFile(classPathDir + "/"
							+ classPathEntry);
					if (tempFile.isDirectory())
					{
						recurseClassPathEntry(tempFile);
					}
					else
					{
						loggedClassPathEntries.add(tempFile);
					}
				}
			}
		}

		/**
		 * Checks if the class pool has been updated
		 * 
		 * @return true, if is updated
		 */
		public boolean isUpdated()
		{
			for (LoggedFile lFile : loggedClassPathEntries)
			{
				if (lFile.isUpdated())
				{
					return true;
				}
			}
			return false;
		}

		public void logUpdate()
		{
			for (LoggedFile lFile : loggedClassPathEntries)
			{
				lFile.logTimeStamp();
			}

		}
	}

	/**
	 * This set keeps track of all of the desktops associated with this virtual
	 * machine.
	 */
	private Set<String> desktops;
	/**
	 * The local root directory for the virtual machine.
	 */
	private String localRoot;

	/**
	 * The Enum PrinterType.
	 */
	public enum PrinterType
	{

		/** The OUT. */
		OUT,
		/** The ERROR. */
		ERROR
	};

	private String loaderStamp;
	/**
	 * The protected Info Stream tags for VM provided streams.
	 */
	public static final String standardOutTag = "Standard Out";
	public static final String standardErrorTag = "Standard Error";
	public static final String logTag = "CloudSpace Logs";
	/**
	 * The Standard Out stream and Printer. The printer is pre wrapped just for
	 * convenience this allows a user to perform VM.out to print to the VM
	 * specfic standard out
	 */
	private final InfoStream stdOut = new InfoStream(standardOutTag);
	public final PrintStream out = new PrintStream(stdOut);
	/** See above for reasoning */
	private final InfoStream stdErr = new InfoStream(standardErrorTag);
	public final PrintStream err = new PrintStream(stdErr);
	private final InfoStream stdLog = new InfoStream(logTag);
	public final PrintStream log = new PrintStream(stdLog);

	/** The mapping from local Directories (unique to each VM) to cloudspaceVMs. */
	private static Map<String, VM> tagToVM = new HashMap<String, VM>();

	/**
	 * The current loader for the VM. This is updated when classes have been
	 * changed in the classpool.
	 */
	private Loader vmLoader;
	private LoggedClassPool vmPool;

	/** The thread current vm. */
	private static ThreadLocal<VM> threadCurrentVM = new ThreadLocal<VM>();
	private static ThreadLocal<String> desktopID = new ThreadLocal<String>();

	public static void setDesktopID(String id)
	{
		desktopID.set(id);
	}

	public static String getDesktopID()
	{

		return desktopID.get();

	}

	/**
	 * Register a thread with this VM.
	 */
	public void registerThread()
	{
		try
		{
			desktopID.set(Executions.getCurrent().getDesktop().getId());
		}
		catch (NullPointerException e)
		{
			desktopID.set("");
		}
		threadCurrentVM.set(this);
	}

	public void registerThread(String dID)
	{
		threadCurrentVM.set(this);
		desktopID.set(dID);
	}

	/**
	 * This removes a thread from registration with this vm.
	 */
	public static void deregisterThread()
	{
		threadCurrentVM.set(null);
	}

	/**
	 * Gets the VM the current workign thread is tagged in.
	 * 
	 * @return the vm tag on the current working thread.
	 */
	public static VM getThreadTag()
	{
		return threadCurrentVM.get();
	}

	/**
	 * This method looks up the current VM context a class is acting in. It uses
	 * ZK desktop and page information to determine which vm instance a thread
	 * is acting in.
	 * 
	 * @return the instance of the vm that the thread is working in
	 */
	public static VM currentVM()
	{
		String currentPage = Executions.getCurrent().getDesktop()
				.getRequestPath();
		String currentDir = getBaseDir(currentPage);
		String localRoot = Executions.getCurrent().getDesktop().getWebApp()
				.getRealPath(currentDir);
		return currentVM(localRoot);

	}

	public static VM currentVM(String currentPath)
	{
		return init(currentPath);
	}

	/**
	 * Initializes a cloudspace vm.
	 * 
	 * @param desktopID
	 *            the desktop id
	 * @param localRoot
	 *            the local root
	 * 
	 * @return a new cloudspace vm.
	 */
	private static VM init(String baseDir)
	{
		VM currentVM = tagToVM.get(baseDir);
		if (currentVM == null)
		{
			currentVM = new VM(baseDir);
		}
		else
		{
			// currentVM.updateLoader();
		}
		return currentVM;
	}

	/**
	 * This private member requests an update to the Class Loader registered to
	 * this virtual Machine. The update will only happen if a class in the class
	 * pool has been updated.
	 */
	private void updateLoader()
	{
		synchronized (vmPool)
		{
			if (vmPool.isUpdated())
			{
				updateLoader0();

			}
		}

	}

	/**
	 * Gets the base dir of a page. This method assumes a
	 * www.webpage.domain/<UNIQUE PAGE TAG>/...... If the format differs from
	 * this, cloudspace will not work.
	 * 
	 * @param currentPage
	 *            string representing the current request path.
	 * 
	 * @return the base dir representation of a page request.
	 */
	private static String getBaseDir(String currentPage)
	{
		int index = currentPage.indexOf('/');
		int index2 = currentPage.lastIndexOf("/");
		String callPath = currentPage.substring(index, index2);
		return callPath;
	}

	/**
	 * Instantiates a new cloudspace vm. Using a localRoot and desktop ID.
	 * 
	 * @param vmContext
	 *            the desk id
	 * @param localRoot
	 *            the local root
	 */
	public VM(String baseDir)
	{
		desktops = new HashSet<String>();
		localRoot = baseDir;
		updateLoader0();
		tagToVM.put(baseDir, this);
	}

	private void updateLoader0()
	{
		this.log.println("Source code has changed, server has been updated!");
		ClassLoader parentLoader = Thread.currentThread()
				.getContextClassLoader();
		ClassPool proxyPool = JavassistBootloader.getProxyPool();
		vmPool = new LoggedClassPool(proxyPool);

		try
		{
			if (Executions.getCurrent() != null)
			{
				vmPool.appendClassPath(Executions.getCurrent().getDesktop()
						.getWebApp().getRealPath("/WEB-INF/classes"));
				vmPool.appendClassPath(Executions.getCurrent().getDesktop()
						.getWebApp().getRealPath("/WEB-INF/pagelib"));
				vmPool.appendClassPath(Executions.getCurrent().getDesktop()
						.getWebApp().getRealPath("/WEB-INF/lib/*"));
			}
			vmPool.appendClassPath(localRoot);
			vmPool.appendClassPath(localRoot + "/*");
			vmPool.appendSystemPath();
		}
		catch (NotFoundException e)
		{
			System.err
					.println("Class pool could not initialize because of missing directories");
		}
		vmPool.logUpdate();
		vmLoader = new Loader(parentLoader, vmPool);
		loaderStamp = Calendar.getInstance().getTime().toString();
		vmLoader.delegateLoadingOf("org.zkoss.");
		vmLoader.delegateLoadingOf("cloudspace.");
		vmLoader.delegateLoadingOf("bsh.");
		try
		{
			JavassistBootloader.translateClasses(vmLoader, vmPool);
		}
		catch (MalformedConfigurationException e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * Prepares an interpreter for use in the current context.
	 * 
	 * @param ip
	 */
	public void prepareInterpreter(Interpreter ip)
	{
		updateLoader();
		ip.setClassLoader(vmLoader);
	}

	/**
	 * Gets the Info Stream operating as a backend to System.out.
	 * 
	 * @return InfoStream System.out
	 */
	public InfoStream getStdOut()
	{
		return stdOut;
	}

	/**
	 * Gets the Info Stream operating as a backend to System.err.
	 * 
	 * @return InfoStream System.err
	 */
	public InfoStream getError()
	{
		return stdErr;
	}

	/**
	 * This gets a VM hardwired stream according to a printer type. Currently,
	 * only system.out and system.err are hardwired.
	 * 
	 * @param streamID
	 * @return the protected VM stream corresponding to the passed printer type.
	 */
	public PrintStream getStream(PrinterType streamID)
	{
		switch (streamID)
		{
		case OUT:
			return out;
		case ERROR:
			return err;
		default:
			return null;
		}
	}

	/**
	 * Get the unique local root for this VM. A VM only has one local root and
	 * it does not share this local root with any other VM.
	 * 
	 * @return The root directory for the VM.
	 */
	public String getLocalRoot()
	{
		return localRoot;
	}

	/**
	 * Gets the class pool for this VM
	 * 
	 * @return the VM's class pool
	 */
	public ClassPool getClassPool()
	{
		return vmPool;
	}

	public boolean isLocalClass(String classname)
	{
		URL foundClass = vmPool.find(classname);
		String fileName = foundClass.getFile();
		// _test.wrap is a special package name for wrapping test
		// classes.
		return (fileName.startsWith(VM.getThreadTag().getLocalRoot()) || classname
				.startsWith("_test.wrap"));
	}

	/**
	 * Gets the class loader used for all bean shell interpreters in this VM's
	 * Context
	 * 
	 * @return the current Context's class loader
	 */
	public Loader getLoader()
	{
		return vmLoader;
	}

	// Static init block. This sets up the SystemStreamMultiplexer to start
	// intercepting system.out and system.err
	static
	{
		if (!(System.out instanceof SystemStreamMultiplexer))
		{
			SystemStreamMultiplexer ssm = new SystemStreamMultiplexer(
					System.out);
			ssm.setStream(PrinterType.OUT);
			System.setOut(ssm);
		}
		if (!(System.err instanceof SystemStreamMultiplexer))
		{
			SystemStreamMultiplexer ssmErr = new SystemStreamMultiplexer(
					System.err);
			ssmErr.setStream(PrinterType.ERROR);
			System.setErr(ssmErr);
		}
	}

	/**
	 * Gets the Info Stream representing the CloudSpace Log.
	 * 
	 * @return the protected InfoStream Log
	 */
	public InfoStream getLog()
	{
		return stdLog;
	}

	/**
	 * Get the last time the loader was initialized.
	 * 
	 * @return the current Loader timestamp
	 */
	public String getLoaderTime()
	{
		return loaderStamp;
	}

	/**
	 * Registers a desktop to the current VM.
	 * 
	 * @param dID
	 *            Desktop ID.
	 */
	public void addDesktop(String dID)
	{
		desktops.add(dID);
	}

	/**
	 * de registers a Desktop ID from the Virtual Machine.
	 * 
	 * @param dID
	 *            the Desktop ID to remove.
	 */
	public void removeDesktop(String dID)
	{
		desktops.remove(dID);
	}

	/**
	 * Gets the number desktops registered to this VM.
	 * 
	 * @return the number desktops
	 */
	public int getNumberDesktops()
	{
		return desktops.size();
	}
}
